<?php
/**
 * 数据库类 - MySQLi类
 */

require_once dirname(__FILE__) . '/Exception.class.php';

class DB_MySQLi extends DB
{
    /**
     * MySQLi构造函数
     *
     * @param array $dbInfo 数据库配置信息
     * @param string $dbKey db的key
     * @param string $fetchMode 返回的数据格式
     */
    public function __construct(&$dbInfo, $dbKey, $fetchMode)
    {
        $this->dbKey = $dbKey;
        $this->dsn = &$dbInfo;
        $this->fecthMode = $fetchMode;
    }

    public function haConnect($hostList, $dbUser, $dbPass, $dbName, $dbPort)
    {

        // 所有host均连接失败才视为失败
        foreach ($hostList as $host) {
            $this->uConn = mysqli_connect($host, $dbUser, $dbPass, $dbName, $dbPort);
            if (!$this->uConn) {
                continue;
            } else {
                break;
            }
        }

    }

    /**
     * 连接数据库
     *
     * 连接数据库之前可能需要改变DSN，一般不建议使用此方法
     *
     * @param string $type 选择连接主服务器或者从服务器
     * @return boolean
     * @throws DB_Exception
     */
    public function connect($type = 'slave')
    {
        global $_configs;

        if ($type == 'master' || !isset($this->dsn['slave'])) {
            $dbHost = isset($this->dsn['master']) ? $this->dsn['master']['dbHost'] : $this->dsn['dbHost'];
            $dbPort = isset($this->dsn['master']) ? $this->dsn['master']['dbPort'] : $this->dsn['dbPort'];
            $dbName = isset($this->dsn['master']) ? $this->dsn['master']['dbName'] : $this->dsn['dbName'];
            $dbUser = isset($this->dsn['master']) ? $this->dsn['master']['dbUser'] : $this->dsn['dbUser'];
            $dbPass = isset($this->dsn['master']) ? $this->dsn['master']['dbPass'] : $this->dsn['dbPass'];

            if (!isset($dbPort) || empty($dbPort)) {
                $dbPort = 3306;
            }

            $hostList = array_merge(array($dbHost), $_configs['dbha']);
            $this->haConnect($hostList, $dbUser, $dbPass, $dbName, $dbPort);
            if (!$this->uConn) {
                throw new DB_Exception('更新数据库连接失败');
            }
            if (!isset($this->dsn['slave'])) {
                $this->qConn = &$this->uConn;
            }
            $sql = 'SET NAMES UTF8';
            $this->update($sql);
        } else {
            if (empty($this->dsn['slave'])) {
                $this->connect('master');
                return $this->qConn = &$this->uConn;
            }
            if (empty($_COOKIE[COOKIE_PREFIX . $this->dbKey . 'DbNo'])) {
                $dbNo = array_rand($this->dsn['slave']);
                setcookie(COOKIE_PREFIX . $this->dbKey . 'DbNo', $dbNo, null, COOKIE_PATH, COOKIE_DOMAIN);
            } else {
                $dbNo = $_COOKIE[COOKIE_PREFIX . $this->dbKey . 'DbNo'];
            }
            $dbInfo = $this->dsn['slave'][$dbNo];
            $dbHost = $dbInfo['dbHost'];
            $dbName = $dbInfo['dbName'];
            $dbUser = $dbInfo['dbUser'];
            $dbPass = $dbInfo['dbPass'];
            $this->qConn = mysqli_connect($dbHost, $dbUser, $dbPass);

            if (!$this->qConn) {
                if (!$this->uConn) {
                    $this->connect('slave');
                }
                $this->qConn = &$this->uConn;
                if (!$this->qConn) {
                    throw new DB_Exception('查询数据库连接失败');
                }
            } else {
                if (!mysqli_select_db($this->qConn, $dbName)) {
                    throw new DB_Exception('查询数据库选择失败');
                }
            }
            $sql = 'SET NAMES UTF8';
            $this->update($sql);
        }
        return true;
    }

    /**
     * 关闭数据库连接
     *
     * 一般不需要调用此方法
     */
    public function close()
    {
        if ($this->uConn === $this->qConn) {
            if (is_object($this->uConn)) {
                mysqli_close($this->uConn);
            }
        } else {
            if (is_object($this->uConn)) {
                mysqli_close($this->uConn);
            }
            if (is_object($this->qConn)) {
                mysqli_close($this->qConn);
            }
        }
    }

    /**
     * 执行一个SQL查询
     *
     * 本函数仅限于执行SELECT类型的SQL语句
     *
     * @param string $sql SQL查询语句
     * @param mixed $limit 整型或者字符串类型，如10|10,10
     * @param boolean $quick 是否快速查询
     * @return bool|mysqli_result 返回查询结果资源句柄
     * @throws DB_Exception
     */
    public function query($sql, $limit = null, $quick = false)
    {

        if ($limit != null) {
            if (!preg_match('/^\s*SHOW/i', $sql) && !preg_match('/FOR UPDATE\s*$/i', $sql) && !preg_match('/LOCK IN SHARE MODE\s*$/i', $sql)) {
                $sql = $sql . ' LIMIT ' . $limit;
            }
        }
        $this->sqls[] = $sql;
        $this->qSqls[] = $sql;
        $this->sql = $sql;
        $this->time[count($this->sqls) - 1][] = microtime(true);
        if (!$this->uConn) {
            $this->connect('master');
        }
        if (!$this->qConn) {
            $this->connect('slave');
        }

        if (!mysqli_ping($this->qConn)) {
            $this->connect();
        }
        $this->qrs = mysqli_query($this->qConn, $sql, $quick ? MYSQLI_USE_RESULT : MYSQLI_STORE_RESULT);
        if (!$this->qrs) {
            throw new DB_Exception('查询失败:' . mysqli_error($this->qConn) . '. Errno : ' . mysqli_errno($this->qConn) . ', sql:[$sql]');
        } else {
            $this->time[count($this->sqls) - 1][] = microtime(true);
            $this->queryNum++;
            return $this->qrs;
        }
    }


    /**
     * 获取结果集
     *
     * @param $rs
     * @param int $fetchMode
     * @return array 返回数据集每一行，并将$rs指针下移
     */
    public function fetch($rs, $fetchMode = self::DB_FETCH_DEFAULT)
    {
        switch ($fetchMode) {
            case 1 :
                $fetchMode = self::DB_FETCH_ASSOC;
                break;
            case 2 :
                $fetchMode = self::DB_FETCH_ROW;
                break;
            case 3 :
                $fetchMode = self::DB_FETCH_ARRAY;
                break;
            default :
                $fetchMode = self::DB_FETCH_DEFAULT;
                break;
        }
        return mysqli_fetch_array($rs, $fetchMode);
    }

    /**
     * 执行一个SQL更新
     *
     * 本方法仅限数据库UPDATE操作
     *
     * @param string $sql 数据库更新SQL语句
     * @return boolean
     * @throws DB_Exception
     */
    public function update($sql)
    {
        $this->sql = $sql;
        $this->sqls[] = $this->sql;
        $this->uSqls[] = $this->sql;
        if (!$this->uConn) {
            $this->connect('master');
        }

        if (!mysqli_ping($this->uConn)) {
            $this->connect();
        }

        $this->urs = mysqli_query($this->uConn, $sql);

        if (!$this->urs) {
            throw new DB_Exception('更新失败:' . mysqli_error($this->uConn) . '. Errno : ' . mysqli_errno($this->uConn) . ', sql:[$sql]');
        } else {
            $this->updateNum++;
            return $this->urs;
        }
    }

    /**
     * 返回SQL语句执行结果集中的第一行第一列数据
     *
     * @param string $sql 需要执行的SQL语句
     * @return mixed 查询结果
     * @throws DB_Exception
     */
    public function getOne($sql)
    {
        if (!$rs = $this->query($sql, 1, true)) {
            return 0;
        }
        $row = $this->fetch($rs, self::DB_FETCH_ROW);
        $this->free($rs);
        return $row[0];
    }

    /**
     * 返回SQL语句执行结果集中的第一列数据
     *
     * @param string $sql 需要执行的SQL语句
     * @param mixed $limit 整型或者字符串类型，如10|10,10
     * @return bool|array 结果集数组
     * @throws DB_Exception
     */
    public function getCol($sql, $limit = null)
    {
        if (!$rs = $this->query($sql, $limit, true)) {
            return false;
        }
        $result = array();
        while ($rows = $this->fetch($rs, self::DB_FETCH_ROW)) {
            $result[] = $rows[0];
        }
        $this->free($rs);
        return $result;
    }

    /**
     * 返回SQL语句执行结果中的第一行数据
     *
     * @param string $sql 需要执行的SQL语句
     * @param int $fetchMode 返回的数据格式
     * @return bool|array
     * @throws DB_Exception
     */
    public function getRow($sql, $fetchMode = self::DB_FETCH_DEFAULT)
    {
        if (!$rs = $this->query($sql, 1, true)) {
            return false;
        }
        $row = $this->fetch($rs, $fetchMode);
        $this->free($rs);
        return $row;
    }

    /**
     * 返回SQL语句执行结果中的所有行数据
     *
     * @param string $sql 需要执行的SQL语句
     * @param mixed $limit 整型或者字符串类型，如10|10,10
     * @param int $fetchMode 返回的数据格式
     * @return bool|array 结果集二维数组
     * @throws DB_Exception
     */
    public function getAll($sql, $limit = null, $fetchMode = self::DB_FETCH_DEFAULT)
    {
        if (!$rs = $this->query($sql, $limit, true)) {
            return false;
        }
        $allRows = array();
        while ($row = $this->fetch($rs, $fetchMode)) {
            $allRows[] = $row;
        }
        $this->free($rs);
        return $allRows;
    }

    /**
     * 按主键或者唯一索引取数据,自动创建cache
     * select * from $table where $col = $value
     *
     * @param string $table 表名
     * @param string $col 列名.必须是主键或者唯一索引
     * @param $value
     * @return array|bool
     * @throws DB_Exception
     */
    public function cacheGetRow($table, $col, $value)
    {
        global $cache;

        if (empty($table) || empty($col) || empty($value)) {
            return false;
        }

        // 拼接得到缓存的key
        $dbName = isset($this->dsn['master']) ? $this->dsn['master']['dbName'] : $this->dsn['dbName'];
        $cacheKey = $dbName . '_' . $table . '_' . $col . '_' . $value;

        // 从cache中取数据
        $cacheData = $cache->getData($cacheKey);
        if (!empty($cacheData) && $cacheData !== false) {
            // 取到数据则直接返回
            return $cacheData;
        } else {
            // 没有取到数据则从DB取,并建立cache
            $sql = "select * from {$table} where {$col} = '{$value}'";
            if (!$rs = $this->query($sql, 1, true)) {
                return false;
            }
            $row = $this->fetch($rs);
            $this->free($rs);

            // 建cache,默认7天过期
            $cache->setData($cacheKey, $row, 7 * 86400);

            return $row;
        }
    }

    /**
     * 更新数据并失效cache
     * update $table set $setData where $col = $value
     *
     * @param $table
     * @param $setData
     * @param $col
     * @param $value
     * @return bool|mysqli_result
     * @throws DB_Exception
     */
    public function cacheUpdate($table, $setData, $col, $value)
    {
        global $cache;

        if (empty($table) || empty($setData) || empty($col) || empty($value)) {
            return false;
        }

        // 拼接sql
        foreach ($setData as $setCol => $setValue) {
            $setArr[] = $setCol . " = '" . $setValue . "'";
        }
        $setStr = implode(',', $setArr);

        if (is_array($value)) {
            $inStr = implode("','", $value);
            $whereStr = " where {$col} in ('{$inStr}')";
        } else {
            $whereStr = " where {$col} = '{$value}'";
        }
        $sql = "update {$table} set {$setStr} {$whereStr}";

        // 执行sql
        $this->sql = $sql;
        $this->sqls[] = $this->sql;
        $this->uSqls[] = $this->sql;
        if (!$this->uConn) {
            $this->connect('master');
        }

        if (!mysqli_ping($this->uConn)) {
            $this->connect();
        }

        $this->urs = mysqli_query($this->uConn, $sql);

        if (!$this->urs) {
            throw new DB_Exception('更新失败:' . mysqli_error($this->uConn) .
                '. Errno : ' . mysqli_errno($this->uConn) . ', sql:[$sql]');
        } else {
            // 设置cache失效
            $dbName = isset($this->dsn['master']) ? $this->dsn['master']['dbName'] : $this->dsn['dbName'];
            if (is_array($value)) {
                foreach ($value as $v) {
                    $cacheKey = $dbName . '_' . $table . '_' . $col . '_' . $v;
                    $cache->delData($cacheKey);
                }
            } else {
                $cacheKey = $dbName . '_' . $table . '_' . $col . '_' . $value;
                $cache->delData($cacheKey);
            }

            $this->updateNum++;
            return $this->urs;
        }
    }

    /**
     * 设置是否开启事务(是否自动提交)
     *
     * 当设置为false的时候,即开启事务处理模式,表类型应该为INNODB
     *
     * @param boolean $mode
     * @return boolean
     * @throws DB_Exception
     */
    public function autoCommit($mode = false)
    {
        if (!$this->uConn) {
            $this->connect('master');
        }
        return mysqli_autocommit($this->uConn, $mode);
    }

    /**
     * 提交执行的SQL
     *
     * 当开启事务处理后,要手动提交执行的SQL语句
     *
     * @return boolean
     */
    public function commit()
    {
        return mysqli_commit($this->uConn);
    }

    /**
     * 回滚
     *
     * 当开启事务处理后,有需要的时候进行回滚
     *
     * @return boolean
     */
    public function rollback()
    {
        return mysqli_rollback($this->uConn);
    }

    /**
     * 返回最近一次查询返回的结果集条数
     *
     * @param $rs
     * @return int
     */
    public function rows($rs)
    {
        return mysqli_num_rows($rs);
    }

    public function affectRows()
    {
        return mysqli_affected_rows($this->qConn);
    }


    /**
     * 获取最近一次query影响的行数
     *
     * @return int
     */

    public function nums()
    {
        return mysqli_affected_rows($this->qConn);
    }

    /**
     * 返回最近一次插入语句的自增长字段的值
     *
     * @return int
     */
    public function lastID()
    {
        return mysqli_insert_id($this->uConn);
    }

    /**
     * 释放当前查询结果资源句柄
     * @param $rs
     */
    public function free($rs)
    {
        if ($rs) {
            return mysqli_free_result($rs);
        }
    }

    /**
     * 转义需要插入或者更新的字段值
     *
     * 在所有查询和更新的字段变量都需要调用此方法处理数据
     *
     * @param mixed $str 需要处理的变量
     * @return mixed 返回转义后的结果
     */
    public function escape($str)
    {
        if (is_array($str)) {
            foreach ($str as $key => $value) {
                $str[$key] = $this->escape($value);
            }
        } else {
            return addslashes($str);
        }
        return $str;
    }

    /**
     * 析构函数，暂时不需要做什么处理
     */
    public function __destruct()
    {
    }
}
